//    Copyright (C) Kherty.  All rights reserved.
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using OpenLS.Spreadsheet.Extensibility;

namespace OpenLS.Spreadsheet.AnalysisToolPak
{
    [AddInFunctions]
    [AllowStringToDateTimeConversion]
    [AllowDoubleToDateTimeConversion]
    public static class DateFunctions
    {
        public static object DateDif([AccessCellValue] DateTime start_date, [AccessCellValue] DateTime end_date, string unit)
        {
            if (start_date > end_date)
                return Errors.NumError;
            TimeSpan sp = end_date - start_date;
            switch (unit.ToUpper())
            {
                case "Y":
                    int result = end_date.Year - start_date.Year;
                    DateTime dt = DateInYear(end_date, start_date.Year);
                    if (dt < start_date)    
                        result--;
                    return result;
                case "M":
                    result = end_date.Year - start_date.Year;
                    result = result*12;
                    result = result + end_date.Month - start_date.Month;
                    return result;
                case "D":
                    return sp.TotalDays;
                case "MD":
                    DateTime  newDate = new DateTime(start_date.Year, end_date.Month, end_date.Day);
                    if (start_date < newDate)
                        return DateDif(start_date, newDate, "M");
                    return
                        DateDif(newDate, start_date, "M");
                case "YM":
                    return DateDif(start_date, DateInYear(end_date, start_date.Year), "M");
                case "YD":
                    return DateDif(start_date, DateInYear(end_date, start_date.Year), "D");
                default:
                    return Errors.NumError;
            }
        }

        private static DateTime DateInYear(DateTime dt, int yearDate)
        {
            if (DateTime.IsLeapYear(dt.Year) && !DateTime.IsLeapYear(yearDate) && dt.Month == 2 && dt.Day == 29)
                dt = new DateTime(dt.Year, dt.Month, 28);
            return new DateTime(yearDate, dt.Month, dt.Day);
        }

        
        /*public static DateTime WorkDayOld(DateTime serial_number, int days, [Optional(null)]IArray holidays)
        {
            DateTime 
            
        }*/
        class Enumerable
        {
            public static int Count<T>(IEnumerable<T> source, Predicate<T> predicate)
            {
                Contract.Requires(source != null);
                int result = 0;
                foreach (var enumerable in source)
                {
                    if (predicate(enumerable))
                        result++;
                }
                return result;
            }

        }
        [AddInFunction(Name="WORKDAY.INTL")]
        public static DateTime WORKDAY_INTL(DateTime a2, int days, object type, params DateTime[] holidays)
        {
            DateTime dt = a2;
            int remaidingDays = days;
            var holidaysTable = new Dictionary<DateTime, bool>();
            foreach (var h in holidays)
                holidaysTable.Add(h, true);
            Func<DateTime, bool> isWeekEnd;
            if (type is ICell)
                type = ((ICell) type).Value;
            if (type is double)
                type = (int)(double) type;
            if (type is int)
                type = (WeekEndType) (int) type;
            if (type is WeekEndType)
                isWeekEnd = d => IsWeekEnd(d, (WeekEndType) type);
            else if (type is string)
            {
                string s = (string) type;
                if (s.Length != 7 || Enumerable.Count(s, c => c == '1' || c == '0') != 7)
                    throw new Exception();
                isWeekEnd = d =>
                                {
                                    int i = (int) dt.DayOfWeek - 1;
                                    if (i < 0) i += 7;
                                    return s[i] == '1';
                                };
            }
            else
            {
                throw new ErrorException(Errors.NameError);
            }
            Func<DateTime, bool> isOK = delegate(DateTime d)
                                            {
                                                if (isWeekEnd(d))
                                                    return false;
                                                return !holidaysTable.ContainsKey(d);
                                            };
            while (remaidingDays != 0)
            {
                dt = dt.AddDays(1);
                if (isOK(dt))
                {
                    remaidingDays += -Math.Sign(remaidingDays);
                }
            }
            return dt;

        }

        public static double WorkDay([WorksheetContext]IWorksheet worksheet, DateTime a2, int days, params DateTime[] holidays)
        {
            DateTime dt = a2;
            int remaidingDays = days;
            Func<DateTime, bool> isOK = delegate(DateTime d)
                                            {
                                                switch (d.DayOfWeek)
                                                {
                                                    case DayOfWeek.Sunday:
                                                    case DayOfWeek.Saturday:
                                                        return false;
                                                }
                                                foreach (DateTime h in holidays)
                                                {
                                                    if (d == h)
                                                        return false;
                                                }
                                                return true;
                                            };
            while (remaidingDays != 0)
            {
                dt = dt.AddDays(1);
                if (isOK(dt))
                {
                    remaidingDays += -Math.Sign(remaidingDays);
                }
            }
            return worksheet.Workbook.DateTimeToDouble(dt);
        }

        private static bool IsWeekEnd(DateTime dt, WeekEndType type)
            {
                DayOfWeek dayOfWeek = dt.DayOfWeek;
                switch (dayOfWeek)
                {
                    case DayOfWeek.Friday:
                        switch (type)
                        {
                            case WeekEndType.Friday:
                            case WeekEndType.FridaySaturday:
                            case WeekEndType.ThursdayFriday:
                                return true;
                        }
                        break;
                    case DayOfWeek.Monday:
                        switch (type)
                        {
                            case WeekEndType.Monday:
                            case WeekEndType.MondayTuesday:
                            case WeekEndType.SundayMonday:
                                return true;
                        }
                        break;
                    case DayOfWeek.Saturday:
                        switch (type)
                        {
                            case WeekEndType.FridaySaturday:
                            case WeekEndType.Saturday:
                            case WeekEndType.SaturdaySunday:
                                return true;
                        }
                        break;
                    case DayOfWeek.Sunday:
                        switch (type)
                        {
                            case WeekEndType.SaturdaySunday:
                            case WeekEndType.Sunday:
                            case WeekEndType.SundayMonday:
                                return true;
                        }
                        break;
                    case DayOfWeek.Thursday:
                        switch (type)
                        {
                            case WeekEndType.Thursday:
                            case WeekEndType.ThursdayFriday:
                            case WeekEndType.WednesdayThursday:
                                return true;
                        }
                        break;
                    case DayOfWeek.Tuesday:
                        switch (type)
                        {
                            case WeekEndType.MondayTuesday:
                            case WeekEndType.Tuesday:
                            case WeekEndType.TuesdayWednesday:
                                return true;
                        }
                        break;
                    case DayOfWeek.Wednesday:
                        switch (type)
                        {
                            case WeekEndType.TuesdayWednesday:
                            case WeekEndType.Wednesday:
                            case WeekEndType.WednesdayThursday:
                                return true;
                        }
                        break;
                }
                throw new NotSupportedException();
            }
        

        public enum WeekEndType
        {
            SaturdaySunday = 1,
            SundayMonday = 2,
            MondayTuesday = 3,
            TuesdayWednesday = 4,
            WednesdayThursday = 5,
            ThursdayFriday = 6,
            FridaySaturday = 7,
            Sunday = 11,
            Monday = 12,
            Tuesday = 13,
            Wednesday = 14,
            Thursday = 15,
            Friday = 16,
            Saturday = 17
        }


        public static int WeekNum(DateTime serial_number, [Optional(1)] int return_type)
        {
            DateTime startOfYear = new DateTime(serial_number.Year, 1, 1);
            var yearStartDay = startOfYear.DayOfWeek;
            DayOfWeek weekStart = DayOfWeek.Sunday;
            switch (return_type)
            {
                case 1 :
                    weekStart = DayOfWeek.Sunday;
                    break;
                case 2:
                    weekStart = DayOfWeek.Monday;
                    break;
                case 11:
                    weekStart = DayOfWeek.Monday;
                    break;
                case 12:
                    weekStart = DayOfWeek.Tuesday;
                    break;
                case 13:
                    weekStart = DayOfWeek.Wednesday;
                    break;
                case 14:
                    weekStart = DayOfWeek.Thursday;
                    break;
                case 15:
                    weekStart = DayOfWeek.Friday;
                    break;
                case 16:
                    weekStart = DayOfWeek.Saturday;
                    break;
                case 17:
                    weekStart = DayOfWeek.Sunday;
                    break;
            }
            int dayBack = yearStartDay - weekStart;
            if (dayBack < 0)
                dayBack += 7;
            Contract.Assert(dayBack >= 0);
            Contract.Assert(dayBack < 7);
            DateTime start = startOfYear.AddDays( -dayBack);
            var diff = serial_number - start;
            return 1 + (int)(Math.Truncate(diff.TotalDays)/7);
        }

       

        public static double YearFrac(DateTime from, DateTime to, [Optional(DateBasisType.Nasd30360), AllowDoubleToEnumConversion] DateBasisType basis)
{
            var b = DateBasis.GetBasis(basis);
            return b.YearFrac(from, to);
}




        [AllowDoubleToDateTimeConversion]
        public static int NetWorkDays(DateTime start_date, DateTime end_date,
                                      [TreatArrayValueColumnAsDateTimeArray] [Optional(null)] params DateTime[] holidays)
        {
            start_date = start_date.Date;
            end_date = end_date.Date;
            if (end_date < start_date)
                return -NetWorkDays(end_date, start_date, holidays);
            int day = DayOf(start_date);
            day = 7 - day;
            day = day%7;
            DateTime nextSunday = start_date.AddDays(day);

            day = DayOf(end_date);
            DateTime previousSunday = end_date.AddDays(-day);
            TimeSpan ts = previousSunday - nextSunday;
            int result = (int) ts.TotalDays;
            result = result/7*5;

            result += (6 - DayOf(start_date))%6;
            result += Math.Min(DayOf(end_date), 5);
            if (holidays != null)
            {
                foreach (DateTime dt in holidays)
                {
                    switch (dt.DayOfWeek)
                    {
                        case DayOfWeek.Saturday:
                        case DayOfWeek.Sunday:
                            continue;
                    }
                    if (start_date <= dt && dt <= end_date)
                        result--;
                }
            }
            return result;
        }

        private static int DayOf(DateTime dt)
        {
            return (int) dt.DayOfWeek;
        }
    }
}